home *** CD-ROM | disk | FTP | other *** search
/ PC go! 2018 January / PCgo 01-2018 CD-ROM Germany.iso / nw.pak / Unnamed File 004868.txt < prev    next >
Encoding:
Text File  |  2015-07-29  |  63.6 KB  |  2,023 lines

  1. // Copyright (c) 2013 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4.  
  5. var USER_MEDIA_TAB_ID = 'user-media-tab-id';
  6.  
  7. var tabView = null;
  8. var ssrcInfoManager = null;
  9. var peerConnectionUpdateTable = null;
  10. var statsTable = null;
  11. var dumpCreator = null;
  12. /** A map from peer connection id to the PeerConnectionRecord. */
  13. var peerConnectionDataStore = {};
  14. /** A list of getUserMedia requests. */
  15. var userMediaRequests = [];
  16.  
  17. /** A simple class to store the updates and stats data for a peer connection. */
  18. var PeerConnectionRecord = (function() {
  19.   /** @constructor */
  20.   function PeerConnectionRecord() {
  21.     /** @private */
  22.     this.record_ = {
  23.       constraints: {},
  24.       rtcConfiguration: [],
  25.       stats: {},
  26.       updateLog: [],
  27.       url: '',
  28.     };
  29.   };
  30.  
  31.   PeerConnectionRecord.prototype = {
  32.     /** @override */
  33.     toJSON: function() {
  34.       return this.record_;
  35.     },
  36.  
  37.     /**
  38.      * Adds the initilization info of the peer connection.
  39.      * @param {string} url The URL of the web page owning the peer connection.
  40.      * @param {Array} rtcConfiguration
  41.      * @param {!Object} constraints Media constraints.
  42.      */
  43.     initialize: function(url, rtcConfiguration, constraints) {
  44.       this.record_.url = url;
  45.       this.record_.rtcConfiguration = rtcConfiguration;
  46.       this.record_.constraints = constraints;
  47.     },
  48.  
  49.     /**
  50.      * @param {string} dataSeriesId The TimelineDataSeries identifier.
  51.      * @return {!TimelineDataSeries}
  52.      */
  53.     getDataSeries: function(dataSeriesId) {
  54.       return this.record_.stats[dataSeriesId];
  55.     },
  56.  
  57.     /**
  58.      * @param {string} dataSeriesId The TimelineDataSeries identifier.
  59.      * @param {!TimelineDataSeries} dataSeries The TimelineDataSeries to set to.
  60.      */
  61.     setDataSeries: function(dataSeriesId, dataSeries) {
  62.       this.record_.stats[dataSeriesId] = dataSeries;
  63.     },
  64.  
  65.     /**
  66.      * @param {!Object} update The object contains keys "time", "type", and
  67.      *   "value".
  68.      */
  69.     addUpdate: function(update) {
  70.       var time = new Date(parseFloat(update.time));
  71.       this.record_.updateLog.push({
  72.         time: time.toLocaleString(),
  73.         type: update.type,
  74.         value: update.value,
  75.       });
  76.     },
  77.   };
  78.  
  79.   return PeerConnectionRecord;
  80. })();
  81.  
  82. // The maximum number of data points bufferred for each stats. Old data points
  83. // will be shifted out when the buffer is full.
  84. var MAX_STATS_DATA_POINT_BUFFER_SIZE = 1000;
  85.  
  86. // Copyright 2013 The Chromium Authors. All rights reserved.
  87. // Use of this source code is governed by a BSD-style license that can be
  88. // found in the LICENSE file.
  89.  
  90. /**
  91.  * A TabView provides the ability to create tabs and switch between tabs. It's
  92.  * responsible for creating the DOM and managing the visibility of each tab.
  93.  * The first added tab is active by default and the others hidden.
  94.  */
  95. var TabView = (function() {
  96.   'use strict';
  97.  
  98.   /**
  99.    * @constructor
  100.    * @param {Element} root The root DOM element containing the tabs.
  101.    */
  102.   function TabView(root) {
  103.     this.root_ = root;
  104.     this.ACTIVE_TAB_HEAD_CLASS_ = 'active-tab-head';
  105.     this.ACTIVE_TAB_BODY_CLASS_ = 'active-tab-body';
  106.     this.TAB_HEAD_CLASS_ = 'tab-head';
  107.     this.TAB_BODY_CLASS_ = 'tab-body';
  108.  
  109.     /**
  110.      * A mapping for an id to the tab elements.
  111.      * @type {!Object<string, !TabDom>}
  112.      * @private
  113.      */
  114.     this.tabElements_ = {};
  115.  
  116.     this.headBar_ = null;
  117.     this.activeTabId_ = null;
  118.     this.initializeHeadBar_();
  119.   }
  120.  
  121.   // Creates a simple object containing the tab head and body elements.
  122.   function TabDom(h, b) {
  123.     this.head = h;
  124.     this.body = b;
  125.   }
  126.  
  127.   TabView.prototype = {
  128.     /**
  129.      * Adds a tab with the specified id and title.
  130.      * @param {string} id
  131.      * @param {string} title
  132.      * @return {!Element} The tab body element.
  133.      */
  134.     addTab: function(id, title) {
  135.       if (this.tabElements_[id])
  136.         throw 'Tab already exists: ' + id;
  137.  
  138.       var head = document.createElement('span');
  139.       head.className = this.TAB_HEAD_CLASS_;
  140.       head.textContent = title;
  141.       head.title = title;
  142.       this.headBar_.appendChild(head);
  143.       head.addEventListener('click', this.switchTab_.bind(this, id));
  144.  
  145.       var body = document.createElement('div');
  146.       body.className = this.TAB_BODY_CLASS_;
  147.       body.id = id;
  148.       this.root_.appendChild(body);
  149.  
  150.       this.tabElements_[id] = new TabDom(head, body);
  151.  
  152.       if (!this.activeTabId_) {
  153.         this.switchTab_(id);
  154.       }
  155.       return this.tabElements_[id].body;
  156.     },
  157.  
  158.     /** Removes the tab. @param {string} id */
  159.     removeTab: function(id) {
  160.       if (!this.tabElements_[id])
  161.         return;
  162.       this.tabElements_[id].head.parentNode.removeChild(
  163.           this.tabElements_[id].head);
  164.       this.tabElements_[id].body.parentNode.removeChild(
  165.           this.tabElements_[id].body);
  166.  
  167.       delete this.tabElements_[id];
  168.       if (this.activeTabId_ == id) {
  169.         this.switchTab_(Object.keys(this.tabElements_)[0]);
  170.       }
  171.     },
  172.  
  173.     /**
  174.      * Switches the specified tab into view.
  175.      *
  176.      * @param {string} activeId The id the of the tab that should be switched to
  177.      *     active state.
  178.      * @private
  179.      */
  180.     switchTab_: function(activeId) {
  181.       if (this.activeTabId_ && this.tabElements_[this.activeTabId_]) {
  182.         this.tabElements_[this.activeTabId_].body.classList.remove(
  183.             this.ACTIVE_TAB_BODY_CLASS_);
  184.         this.tabElements_[this.activeTabId_].head.classList.remove(
  185.             this.ACTIVE_TAB_HEAD_CLASS_);
  186.       }
  187.       this.activeTabId_ = activeId;
  188.       if (this.tabElements_[activeId]) {
  189.         this.tabElements_[activeId].body.classList.add(
  190.             this.ACTIVE_TAB_BODY_CLASS_);
  191.         this.tabElements_[activeId].head.classList.add(
  192.             this.ACTIVE_TAB_HEAD_CLASS_);
  193.       }
  194.     },
  195.  
  196.     /** Initializes the bar containing the tab heads. */
  197.     initializeHeadBar_: function() {
  198.       this.headBar_ = document.createElement('div');
  199.       this.root_.appendChild(this.headBar_);
  200.       this.headBar_.style.textAlign = 'center';
  201.     },
  202.   };
  203.   return TabView;
  204. })();
  205.  
  206. // Copyright (c) 2013 The Chromium Authors. All rights reserved.
  207. // Use of this source code is governed by a BSD-style license that can be
  208. // found in the LICENSE file.
  209.  
  210. /**
  211.  * A TimelineDataSeries collects an ordered series of (time, value) pairs,
  212.  * and converts them to graph points.  It also keeps track of its color and
  213.  * current visibility state.
  214.  * It keeps MAX_STATS_DATA_POINT_BUFFER_SIZE data points at most. Old data
  215.  * points will be dropped when it reaches this size.
  216.  */
  217. var TimelineDataSeries = (function() {
  218.   'use strict';
  219.  
  220.   /**
  221.    * @constructor
  222.    */
  223.   function TimelineDataSeries() {
  224.     // List of DataPoints in chronological order.
  225.     this.dataPoints_ = [];
  226.  
  227.     // Default color.  Should always be overridden prior to display.
  228.     this.color_ = 'red';
  229.     // Whether or not the data series should be drawn.
  230.     this.isVisible_ = true;
  231.  
  232.     this.cacheStartTime_ = null;
  233.     this.cacheStepSize_ = 0;
  234.     this.cacheValues_ = [];
  235.   }
  236.  
  237.   TimelineDataSeries.prototype = {
  238.     /**
  239.      * @override
  240.      */
  241.     toJSON: function() {
  242.       if (this.dataPoints_.length < 1)
  243.         return {};
  244.  
  245.       var values = [];
  246.       for (var i = 0; i < this.dataPoints_.length; ++i) {
  247.         values.push(this.dataPoints_[i].value);
  248.       }
  249.       return {
  250.         startTime: this.dataPoints_[0].time,
  251.         endTime: this.dataPoints_[this.dataPoints_.length - 1].time,
  252.         values: JSON.stringify(values),
  253.       };
  254.     },
  255.  
  256.     /**
  257.      * Adds a DataPoint to |this| with the specified time and value.
  258.      * DataPoints are assumed to be received in chronological order.
  259.      */
  260.     addPoint: function(timeTicks, value) {
  261.       var time = new Date(timeTicks);
  262.       this.dataPoints_.push(new DataPoint(time, value));
  263.  
  264.       if (this.dataPoints_.length > MAX_STATS_DATA_POINT_BUFFER_SIZE)
  265.         this.dataPoints_.shift();
  266.     },
  267.  
  268.     isVisible: function() {
  269.       return this.isVisible_;
  270.     },
  271.  
  272.     show: function(isVisible) {
  273.       this.isVisible_ = isVisible;
  274.     },
  275.  
  276.     getColor: function() {
  277.       return this.color_;
  278.     },
  279.  
  280.     setColor: function(color) {
  281.       this.color_ = color;
  282.     },
  283.  
  284.     getCount: function() {
  285.       return this.dataPoints_.length;
  286.     },
  287.     /**
  288.      * Returns a list containing the values of the data series at |count|
  289.      * points, starting at |startTime|, and |stepSize| milliseconds apart.
  290.      * Caches values, so showing/hiding individual data series is fast.
  291.      */
  292.     getValues: function(startTime, stepSize, count) {
  293.       // Use cached values, if we can.
  294.       if (this.cacheStartTime_ == startTime &&
  295.           this.cacheStepSize_ == stepSize &&
  296.           this.cacheValues_.length == count) {
  297.         return this.cacheValues_;
  298.       }
  299.  
  300.       // Do all the work.
  301.       this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count);
  302.       this.cacheStartTime_ = startTime;
  303.       this.cacheStepSize_ = stepSize;
  304.  
  305.       return this.cacheValues_;
  306.     },
  307.  
  308.     /**
  309.      * Returns the cached |values| in the specified time period.
  310.      */
  311.     getValuesInternal_: function(startTime, stepSize, count) {
  312.       var values = [];
  313.       var nextPoint = 0;
  314.       var currentValue = 0;
  315.       var time = startTime;
  316.       for (var i = 0; i < count; ++i) {
  317.         while (nextPoint < this.dataPoints_.length &&
  318.                this.dataPoints_[nextPoint].time < time) {
  319.           currentValue = this.dataPoints_[nextPoint].value;
  320.           ++nextPoint;
  321.         }
  322.         values[i] = currentValue;
  323.         time += stepSize;
  324.       }
  325.       return values;
  326.     }
  327.   };
  328.  
  329.   /**
  330.    * A single point in a data series.  Each point has a time, in the form of
  331.    * milliseconds since the Unix epoch, and a numeric value.
  332.    * @constructor
  333.    */
  334.   function DataPoint(time, value) {
  335.     this.time = time;
  336.     this.value = value;
  337.   }
  338.  
  339.   return TimelineDataSeries;
  340. })();
  341.  
  342. // Copyright (c) 2013 The Chromium Authors. All rights reserved.
  343. // Use of this source code is governed by a BSD-style license that can be
  344. // found in the LICENSE file.
  345.  
  346.  
  347.  
  348. /**
  349.  * Get the ssrc if |report| is an ssrc report.
  350.  *
  351.  * @param {!Object} report The object contains id, type, and stats, where stats
  352.  *     is the object containing timestamp and values, which is an array of
  353.  *     strings, whose even index entry is the name of the stat, and the odd
  354.  *     index entry is the value.
  355.  * @return {?string} The ssrc.
  356.  */
  357. function GetSsrcFromReport(report) {
  358.   if (report.type != 'ssrc') {
  359.     console.warn("Trying to get ssrc from non-ssrc report.");
  360.     return null;
  361.   }
  362.  
  363.   // If the 'ssrc' name-value pair exists, return the value; otherwise, return
  364.   // the report id.
  365.   // The 'ssrc' name-value pair only exists in an upcoming Libjingle change. Old
  366.   // versions use id to refer to the ssrc.
  367.   //
  368.   // TODO(jiayl): remove the fallback to id once the Libjingle change is rolled
  369.   // to Chrome.
  370.   if (report.stats && report.stats.values) {
  371.     for (var i = 0; i < report.stats.values.length - 1; i += 2) {
  372.       if (report.stats.values[i] == 'ssrc') {
  373.         return report.stats.values[i + 1];
  374.       }
  375.     }
  376.   }
  377.   return report.id;
  378. };
  379.  
  380. /**
  381.  * SsrcInfoManager stores the ssrc stream info extracted from SDP.
  382.  */
  383. var SsrcInfoManager = (function() {
  384.   'use strict';
  385.  
  386.   /**
  387.    * @constructor
  388.    */
  389.   function SsrcInfoManager() {
  390.     /**
  391.      * Map from ssrc id to an object containing all the stream properties.
  392.      * @type {!Object.<string, !Object.<string>>}
  393.      * @private
  394.      */
  395.     this.streamInfoContainer_ = {};
  396.  
  397.     /**
  398.      * The string separating attibutes in an SDP.
  399.      * @type {string}
  400.      * @const
  401.      * @private
  402.      */
  403.     this.ATTRIBUTE_SEPARATOR_ = /[\r,\n]/;
  404.  
  405.     /**
  406.      * The regex separating fields within an ssrc description.
  407.      * @type {RegExp}
  408.      * @const
  409.      * @private
  410.      */
  411.     this.FIELD_SEPARATOR_REGEX_ = / .*:/;
  412.  
  413.     /**
  414.      * The prefix string of an ssrc description.
  415.      * @type {string}
  416.      * @const
  417.      * @private
  418.      */
  419.     this.SSRC_ATTRIBUTE_PREFIX_ = 'a=ssrc:';
  420.  
  421.     /**
  422.      * The className of the ssrc info parent element.
  423.      * @type {string}
  424.      * @const
  425.      */
  426.     this.SSRC_INFO_BLOCK_CLASS = 'ssrc-info-block';
  427.   }
  428.  
  429.   SsrcInfoManager.prototype = {
  430.     /**
  431.      * Extracts the stream information from |sdp| and saves it.
  432.      * For example:
  433.      *     a=ssrc:1234 msid:abcd
  434.      *     a=ssrc:1234 label:hello
  435.      *
  436.      * @param {string} sdp The SDP string.
  437.      */
  438.     addSsrcStreamInfo: function(sdp) {
  439.       var attributes = sdp.split(this.ATTRIBUTE_SEPARATOR_);
  440.       for (var i = 0; i < attributes.length; ++i) {
  441.         // Check if this is a ssrc attribute.
  442.         if (attributes[i].indexOf(this.SSRC_ATTRIBUTE_PREFIX_) != 0)
  443.           continue;
  444.  
  445.         var nextFieldIndex = attributes[i].search(this.FIELD_SEPARATOR_REGEX_);
  446.  
  447.         if (nextFieldIndex == -1)
  448.           continue;
  449.  
  450.         var ssrc = attributes[i].substring(this.SSRC_ATTRIBUTE_PREFIX_.length,
  451.                                            nextFieldIndex);
  452.         if (!this.streamInfoContainer_[ssrc])
  453.           this.streamInfoContainer_[ssrc] = {};
  454.  
  455.         // Make |rest| starting at the next field.
  456.         var rest = attributes[i].substring(nextFieldIndex + 1);
  457.         var name, value;
  458.         while (rest.length > 0) {
  459.           nextFieldIndex = rest.search(this.FIELD_SEPARATOR_REGEX_);
  460.           if (nextFieldIndex == -1)
  461.             nextFieldIndex = rest.length;
  462.  
  463.           // The field name is the string before the colon.
  464.           name = rest.substring(0, rest.indexOf(':'));
  465.           // The field value is from after the colon to the next field.
  466.           value = rest.substring(rest.indexOf(':') + 1, nextFieldIndex);
  467.           this.streamInfoContainer_[ssrc][name] = value;
  468.  
  469.           // Move |rest| to the start of the next field.
  470.           rest = rest.substring(nextFieldIndex + 1);
  471.         }
  472.       }
  473.     },
  474.  
  475.     /**
  476.      * @param {string} sdp The ssrc id.
  477.      * @return {!Object.<string>} The object containing the ssrc infomation.
  478.      */
  479.     getStreamInfo: function(ssrc) {
  480.       return this.streamInfoContainer_[ssrc];
  481.     },
  482.  
  483.     /**
  484.      * Populate the ssrc information into |parentElement|, each field as a
  485.      * DIV element.
  486.      *
  487.      * @param {!Element} parentElement The parent element for the ssrc info.
  488.      * @param {string} ssrc The ssrc id.
  489.      */
  490.     populateSsrcInfo: function(parentElement, ssrc) {
  491.       if (!this.streamInfoContainer_[ssrc])
  492.         return;
  493.  
  494.       parentElement.className = this.SSRC_INFO_BLOCK_CLASS;
  495.  
  496.       var fieldElement;
  497.       for (var property in this.streamInfoContainer_[ssrc]) {
  498.         fieldElement = document.createElement('div');
  499.         parentElement.appendChild(fieldElement);
  500.         fieldElement.textContent =
  501.             property + ':' + this.streamInfoContainer_[ssrc][property];
  502.       }
  503.     }
  504.   };
  505.  
  506.   return SsrcInfoManager;
  507. })();
  508.  
  509. // Copyright (c) 2013 The Chromium Authors. All rights reserved.
  510. // Use of this source code is governed by a BSD-style license that can be
  511. // found in the LICENSE file.
  512.  
  513. //
  514. // This file contains helper methods to draw the stats timeline graphs.
  515. // Each graph represents a series of stats report for a PeerConnection,
  516. // e.g. 1234-0-ssrc-abcd123-bytesSent is the graph for the series of bytesSent
  517. // for ssrc-abcd123 of PeerConnection 0 in process 1234.
  518. // The graphs are drawn as CANVAS, grouped per report type per PeerConnection.
  519. // Each group has an expand/collapse button and is collapsed initially.
  520. //
  521.  
  522. // Copyright (c) 2013 The Chromium Authors. All rights reserved.
  523. // Use of this source code is governed by a BSD-style license that can be
  524. // found in the LICENSE file.
  525.  
  526. /**
  527.  * A TimelineGraphView displays a timeline graph on a canvas element.
  528.  */
  529. var TimelineGraphView = (function() {
  530.   'use strict';
  531.  
  532.   // Maximum number of labels placed vertically along the sides of the graph.
  533.   var MAX_VERTICAL_LABELS = 6;
  534.  
  535.   // Vertical spacing between labels and between the graph and labels.
  536.   var LABEL_VERTICAL_SPACING = 4;
  537.   // Horizontal spacing between vertically placed labels and the edges of the
  538.   // graph.
  539.   var LABEL_HORIZONTAL_SPACING = 3;
  540.   // Horizintal spacing between two horitonally placed labels along the bottom
  541.   // of the graph.
  542.   var LABEL_LABEL_HORIZONTAL_SPACING = 25;
  543.  
  544.   // Length of ticks, in pixels, next to y-axis labels.  The x-axis only has
  545.   // one set of labels, so it can use lines instead.
  546.   var Y_AXIS_TICK_LENGTH = 10;
  547.  
  548.   var GRID_COLOR = '#CCC';
  549.   var TEXT_COLOR = '#000';
  550.   var BACKGROUND_COLOR = '#FFF';
  551.  
  552.   var MAX_DECIMAL_PRECISION = 2;
  553.   /**
  554.    * @constructor
  555.    */
  556.   function TimelineGraphView(divId, canvasId) {
  557.     this.scrollbar_ = {position_: 0, range_: 0};
  558.  
  559.     this.graphDiv_ = $(divId);
  560.     this.canvas_ = $(canvasId);
  561.  
  562.     // Set the range and scale of the graph.  Times are in milliseconds since
  563.     // the Unix epoch.
  564.  
  565.     // All measurements we have must be after this time.
  566.     this.startTime_ = 0;
  567.     // The current rightmost position of the graph is always at most this.
  568.     this.endTime_ = 1;
  569.  
  570.     this.graph_ = null;
  571.  
  572.     // Horizontal scale factor, in terms of milliseconds per pixel.
  573.     this.scale_ = 1000;
  574.  
  575.     // Initialize the scrollbar.
  576.     this.updateScrollbarRange_(true);
  577.   }
  578.  
  579.   TimelineGraphView.prototype = {
  580.     setScale: function(scale) {
  581.       this.scale_ = scale;
  582.     },
  583.  
  584.     // Returns the total length of the graph, in pixels.
  585.     getLength_: function() {
  586.       var timeRange = this.endTime_ - this.startTime_;
  587.       // Math.floor is used to ignore the last partial area, of length less
  588.       // than this.scale_.
  589.       return Math.floor(timeRange / this.scale_);
  590.     },
  591.  
  592.     /**
  593.      * Returns true if the graph is scrolled all the way to the right.
  594.      */
  595.     graphScrolledToRightEdge_: function() {
  596.       return this.scrollbar_.position_ == this.scrollbar_.range_;
  597.     },
  598.  
  599.     /**
  600.      * Update the range of the scrollbar.  If |resetPosition| is true, also
  601.      * sets the slider to point at the rightmost position and triggers a
  602.      * repaint.
  603.      */
  604.     updateScrollbarRange_: function(resetPosition) {
  605.       var scrollbarRange = this.getLength_() - this.canvas_.width;
  606.       if (scrollbarRange < 0)
  607.         scrollbarRange = 0;
  608.  
  609.       // If we've decreased the range to less than the current scroll position,
  610.       // we need to move the scroll position.
  611.       if (this.scrollbar_.position_ > scrollbarRange)
  612.         resetPosition = true;
  613.  
  614.       this.scrollbar_.range_ = scrollbarRange;
  615.       if (resetPosition) {
  616.         this.scrollbar_.position_ = scrollbarRange;
  617.         this.repaint();
  618.       }
  619.     },
  620.  
  621.     /**
  622.      * Sets the date range displayed on the graph, switches to the default
  623.      * scale factor, and moves the scrollbar all the way to the right.
  624.      */
  625.     setDateRange: function(startDate, endDate) {
  626.       this.startTime_ = startDate.getTime();
  627.       this.endTime_ = endDate.getTime();
  628.  
  629.       // Safety check.
  630.       if (this.endTime_ <= this.startTime_)
  631.         this.startTime_ = this.endTime_ - 1;
  632.  
  633.       this.updateScrollbarRange_(true);
  634.     },
  635.  
  636.     /**
  637.      * Updates the end time at the right of the graph to be the current time.
  638.      * Specifically, updates the scrollbar's range, and if the scrollbar is
  639.      * all the way to the right, keeps it all the way to the right.  Otherwise,
  640.      * leaves the view as-is and doesn't redraw anything.
  641.      */
  642.     updateEndDate: function(opt_date) {
  643.       this.endTime_ = opt_date || (new Date()).getTime();
  644.       this.updateScrollbarRange_(this.graphScrolledToRightEdge_());
  645.     },
  646.  
  647.     getStartDate: function() {
  648.       return new Date(this.startTime_);
  649.     },
  650.  
  651.     /**
  652.      * Replaces the current TimelineDataSeries with |dataSeries|.
  653.      */
  654.     setDataSeries: function(dataSeries) {
  655.       // Simply recreates the Graph.
  656.       this.graph_ = new Graph();
  657.       for (var i = 0; i < dataSeries.length; ++i)
  658.         this.graph_.addDataSeries(dataSeries[i]);
  659.       this.repaint();
  660.     },
  661.  
  662.     /**
  663.     * Adds |dataSeries| to the current graph.
  664.     */
  665.     addDataSeries: function(dataSeries) {
  666.       if (!this.graph_)
  667.         this.graph_ = new Graph();
  668.       this.graph_.addDataSeries(dataSeries);
  669.       this.repaint();
  670.     },
  671.  
  672.     /**
  673.      * Draws the graph on |canvas_|.
  674.      */
  675.     repaint: function() {
  676.       this.repaintTimerRunning_ = false;
  677.  
  678.       var width = this.canvas_.width;
  679.       var height = this.canvas_.height;
  680.       var context = this.canvas_.getContext('2d');
  681.  
  682.       // Clear the canvas.
  683.       context.fillStyle = BACKGROUND_COLOR;
  684.       context.fillRect(0, 0, width, height);
  685.  
  686.       // Try to get font height in pixels.  Needed for layout.
  687.       var fontHeightString = context.font.match(/([0-9]+)px/)[1];
  688.       var fontHeight = parseInt(fontHeightString);
  689.  
  690.       // Safety check, to avoid drawing anything too ugly.
  691.       if (fontHeightString.length == 0 || fontHeight <= 0 ||
  692.           fontHeight * 4 > height || width < 50) {
  693.         return;
  694.       }
  695.  
  696.       // Save current transformation matrix so we can restore it later.
  697.       context.save();
  698.  
  699.       // The center of an HTML canvas pixel is technically at (0.5, 0.5).  This
  700.       // makes near straight lines look bad, due to anti-aliasing.  This
  701.       // translation reduces the problem a little.
  702.       context.translate(0.5, 0.5);
  703.  
  704.       // Figure out what time values to display.
  705.       var position = this.scrollbar_.position_;
  706.       // If the entire time range is being displayed, align the right edge of
  707.       // the graph to the end of the time range.
  708.       if (this.scrollbar_.range_ == 0)
  709.         position = this.getLength_() - this.canvas_.width;
  710.       var visibleStartTime = this.startTime_ + position * this.scale_;
  711.  
  712.       // Make space at the bottom of the graph for the time labels, and then
  713.       // draw the labels.
  714.       var textHeight = height;
  715.       height -= fontHeight + LABEL_VERTICAL_SPACING;
  716.       this.drawTimeLabels(context, width, height, textHeight, visibleStartTime);
  717.  
  718.       // Draw outline of the main graph area.
  719.       context.strokeStyle = GRID_COLOR;
  720.       context.strokeRect(0, 0, width - 1, height - 1);
  721.  
  722.       if (this.graph_) {
  723.         // Layout graph and have them draw their tick marks.
  724.         this.graph_.layout(
  725.             width, height, fontHeight, visibleStartTime, this.scale_);
  726.         this.graph_.drawTicks(context);
  727.  
  728.         // Draw the lines of all graphs, and then draw their labels.
  729.         this.graph_.drawLines(context);
  730.         this.graph_.drawLabels(context);
  731.       }
  732.  
  733.       // Restore original transformation matrix.
  734.       context.restore();
  735.     },
  736.  
  737.     /**
  738.      * Draw time labels below the graph.  Takes in start time as an argument
  739.      * since it may not be |startTime_|, when we're displaying the entire
  740.      * time range.
  741.      */
  742.     drawTimeLabels: function(context, width, height, textHeight, startTime) {
  743.       // Draw the labels 1 minute apart.
  744.       var timeStep = 1000 * 60;
  745.  
  746.       // Find the time for the first label.  This time is a perfect multiple of
  747.       // timeStep because of how UTC times work.
  748.       var time = Math.ceil(startTime / timeStep) * timeStep;
  749.  
  750.       context.textBaseline = 'bottom';
  751.       context.textAlign = 'center';
  752.       context.fillStyle = TEXT_COLOR;
  753.       context.strokeStyle = GRID_COLOR;
  754.  
  755.       // Draw labels and vertical grid lines.
  756.       while (true) {
  757.         var x = Math.round((time - startTime) / this.scale_);
  758.         if (x >= width)
  759.           break;
  760.         var text = (new Date(time)).toLocaleTimeString();
  761.         context.fillText(text, x, textHeight);
  762.         context.beginPath();
  763.         context.lineTo(x, 0);
  764.         context.lineTo(x, height);
  765.         context.stroke();
  766.         time += timeStep;
  767.       }
  768.     },
  769.  
  770.     getDataSeriesCount: function() {
  771.       if (this.graph_)
  772.         return this.graph_.dataSeries_.length;
  773.       return 0;
  774.     },
  775.  
  776.     hasDataSeries: function(dataSeries) {
  777.       if (this.graph_)
  778.         return this.graph_.hasDataSeries(dataSeries);
  779.       return false;
  780.     },
  781.  
  782.   };
  783.  
  784.   /**
  785.    * A Graph is responsible for drawing all the TimelineDataSeries that have
  786.    * the same data type.  Graphs are responsible for scaling the values, laying
  787.    * out labels, and drawing both labels and lines for its data series.
  788.    */
  789.   var Graph = (function() {
  790.     /**
  791.      * @constructor
  792.      */
  793.     function Graph() {
  794.       this.dataSeries_ = [];
  795.  
  796.       // Cached properties of the graph, set in layout.
  797.       this.width_ = 0;
  798.       this.height_ = 0;
  799.       this.fontHeight_ = 0;
  800.       this.startTime_ = 0;
  801.       this.scale_ = 0;
  802.  
  803.       // The lowest/highest values adjusted by the vertical label step size
  804.       // in the displayed range of the graph. Used for scaling and setting
  805.       // labels.  Set in layoutLabels.
  806.       this.min_ = 0;
  807.       this.max_ = 0;
  808.  
  809.       // Cached text of equally spaced labels.  Set in layoutLabels.
  810.       this.labels_ = [];
  811.     }
  812.  
  813.     /**
  814.      * A Label is the label at a particular position along the y-axis.
  815.      * @constructor
  816.      */
  817.     function Label(height, text) {
  818.       this.height = height;
  819.       this.text = text;
  820.     }
  821.  
  822.     Graph.prototype = {
  823.       addDataSeries: function(dataSeries) {
  824.         this.dataSeries_.push(dataSeries);
  825.       },
  826.  
  827.       hasDataSeries: function(dataSeries) {
  828.         for (var i = 0; i < this.dataSeries_.length; ++i) {
  829.           if (this.dataSeries_[i] == dataSeries)
  830.             return true;
  831.         }
  832.         return false;
  833.       },
  834.  
  835.       /**
  836.        * Returns a list of all the values that should be displayed for a given
  837.        * data series, using the current graph layout.
  838.        */
  839.       getValues: function(dataSeries) {
  840.         if (!dataSeries.isVisible())
  841.           return null;
  842.         return dataSeries.getValues(this.startTime_, this.scale_, this.width_);
  843.       },
  844.  
  845.       /**
  846.        * Updates the graph's layout.  In particular, both the max value and
  847.        * label positions are updated.  Must be called before calling any of the
  848.        * drawing functions.
  849.        */
  850.       layout: function(width, height, fontHeight, startTime, scale) {
  851.         this.width_ = width;
  852.         this.height_ = height;
  853.         this.fontHeight_ = fontHeight;
  854.         this.startTime_ = startTime;
  855.         this.scale_ = scale;
  856.  
  857.         // Find largest value.
  858.         var max = 0, min = 0;
  859.         for (var i = 0; i < this.dataSeries_.length; ++i) {
  860.           var values = this.getValues(this.dataSeries_[i]);
  861.           if (!values)
  862.             continue;
  863.           for (var j = 0; j < values.length; ++j) {
  864.             if (values[j] > max)
  865.               max = values[j];
  866.             else if (values[j] < min)
  867.               min = values[j];
  868.           }
  869.         }
  870.  
  871.         this.layoutLabels_(min, max);
  872.       },
  873.  
  874.       /**
  875.        * Lays out labels and sets |max_|/|min_|, taking the time units into
  876.        * consideration.  |maxValue| is the actual maximum value, and
  877.        * |max_| will be set to the value of the largest label, which
  878.        * will be at least |maxValue|. Similar for |min_|.
  879.        */
  880.       layoutLabels_: function(minValue, maxValue) {
  881.         if (maxValue - minValue < 1024) {
  882.           this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION);
  883.           return;
  884.         }
  885.  
  886.         // Find appropriate units to use.
  887.         var units = ['', 'k', 'M', 'G', 'T', 'P'];
  888.         // Units to use for labels.  0 is '1', 1 is K, etc.
  889.         // We start with 1, and work our way up.
  890.         var unit = 1;
  891.         minValue /= 1024;
  892.         maxValue /= 1024;
  893.         while (units[unit + 1] && maxValue - minValue >= 1024) {
  894.           minValue /= 1024;
  895.           maxValue /= 1024;
  896.           ++unit;
  897.         }
  898.  
  899.         // Calculate labels.
  900.         this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION);
  901.  
  902.         // Append units to labels.
  903.         for (var i = 0; i < this.labels_.length; ++i)
  904.           this.labels_[i] += ' ' + units[unit];
  905.  
  906.         // Convert |min_|/|max_| back to unit '1'.
  907.         this.min_ *= Math.pow(1024, unit);
  908.         this.max_ *= Math.pow(1024, unit);
  909.       },
  910.  
  911.       /**
  912.        * Same as layoutLabels_, but ignores units.  |maxDecimalDigits| is the
  913.        * maximum number of decimal digits allowed.  The minimum allowed
  914.        * difference between two adjacent labels is 10^-|maxDecimalDigits|.
  915.        */
  916.       layoutLabelsBasic_: function(minValue, maxValue, maxDecimalDigits) {
  917.         this.labels_ = [];
  918.         var range = maxValue - minValue;
  919.         // No labels if the range is 0.
  920.         if (range == 0) {
  921.           this.min_ = this.max_ = maxValue;
  922.           return;
  923.         }
  924.  
  925.         // The maximum number of equally spaced labels allowed.  |fontHeight_|
  926.         // is doubled because the top two labels are both drawn in the same
  927.         // gap.
  928.         var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING;
  929.  
  930.         // The + 1 is for the top label.
  931.         var maxLabels = 1 + this.height_ / minLabelSpacing;
  932.         if (maxLabels < 2) {
  933.           maxLabels = 2;
  934.         } else if (maxLabels > MAX_VERTICAL_LABELS) {
  935.           maxLabels = MAX_VERTICAL_LABELS;
  936.         }
  937.  
  938.         // Initial try for step size between conecutive labels.
  939.         var stepSize = Math.pow(10, -maxDecimalDigits);
  940.         // Number of digits to the right of the decimal of |stepSize|.
  941.         // Used for formating label strings.
  942.         var stepSizeDecimalDigits = maxDecimalDigits;
  943.  
  944.         // Pick a reasonable step size.
  945.         while (true) {
  946.           // If we use a step size of |stepSize| between labels, we'll need:
  947.           //
  948.           // Math.ceil(range / stepSize) + 1
  949.           //
  950.           // labels.  The + 1 is because we need labels at both at 0 and at
  951.           // the top of the graph.
  952.  
  953.           // Check if we can use steps of size |stepSize|.
  954.           if (Math.ceil(range / stepSize) + 1 <= maxLabels)
  955.             break;
  956.           // Check |stepSize| * 2.
  957.           if (Math.ceil(range / (stepSize * 2)) + 1 <= maxLabels) {
  958.             stepSize *= 2;
  959.             break;
  960.           }
  961.           // Check |stepSize| * 5.
  962.           if (Math.ceil(range / (stepSize * 5)) + 1 <= maxLabels) {
  963.             stepSize *= 5;
  964.             break;
  965.           }
  966.           stepSize *= 10;
  967.           if (stepSizeDecimalDigits > 0)
  968.             --stepSizeDecimalDigits;
  969.         }
  970.  
  971.         // Set the min/max so it's an exact multiple of the chosen step size.
  972.         this.max_ = Math.ceil(maxValue / stepSize) * stepSize;
  973.         this.min_ = Math.floor(minValue / stepSize) * stepSize;
  974.  
  975.         // Create labels.
  976.         for (var label = this.max_; label >= this.min_; label -= stepSize)
  977.           this.labels_.push(label.toFixed(stepSizeDecimalDigits));
  978.       },
  979.  
  980.       /**
  981.        * Draws tick marks for each of the labels in |labels_|.
  982.        */
  983.       drawTicks: function(context) {
  984.         var x1;
  985.         var x2;
  986.         x1 = this.width_ - 1;
  987.         x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH;
  988.  
  989.         context.fillStyle = GRID_COLOR;
  990.         context.beginPath();
  991.         for (var i = 1; i < this.labels_.length - 1; ++i) {
  992.           // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
  993.           // lines.
  994.           var y = Math.round(this.height_ * i / (this.labels_.length - 1));
  995.           context.moveTo(x1, y);
  996.           context.lineTo(x2, y);
  997.         }
  998.         context.stroke();
  999.       },
  1000.  
  1001.       /**
  1002.        * Draws a graph line for each of the data series.
  1003.        */
  1004.       drawLines: function(context) {
  1005.         // Factor by which to scale all values to convert them to a number from
  1006.         // 0 to height - 1.
  1007.         var scale = 0;
  1008.         var bottom = this.height_ - 1;
  1009.         if (this.max_)
  1010.           scale = bottom / (this.max_ - this.min_);
  1011.  
  1012.         // Draw in reverse order, so earlier data series are drawn on top of
  1013.         // subsequent ones.
  1014.         for (var i = this.dataSeries_.length - 1; i >= 0; --i) {
  1015.           var values = this.getValues(this.dataSeries_[i]);
  1016.           if (!values)
  1017.             continue;
  1018.           context.strokeStyle = this.dataSeries_[i].getColor();
  1019.           context.beginPath();
  1020.           for (var x = 0; x < values.length; ++x) {
  1021.             // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
  1022.             // horizontal lines.
  1023.             context.lineTo(
  1024.                 x, bottom - Math.round((values[x] - this.min_) * scale));
  1025.           }
  1026.           context.stroke();
  1027.         }
  1028.       },
  1029.  
  1030.       /**
  1031.        * Draw labels in |labels_|.
  1032.        */
  1033.       drawLabels: function(context) {
  1034.         if (this.labels_.length == 0)
  1035.           return;
  1036.         var x = this.width_ - LABEL_HORIZONTAL_SPACING;
  1037.  
  1038.         // Set up the context.
  1039.         context.fillStyle = TEXT_COLOR;
  1040.         context.textAlign = 'right';
  1041.  
  1042.         // Draw top label, which is the only one that appears below its tick
  1043.         // mark.
  1044.         context.textBaseline = 'top';
  1045.         context.fillText(this.labels_[0], x, 0);
  1046.  
  1047.         // Draw all the other labels.
  1048.         context.textBaseline = 'bottom';
  1049.         var step = (this.height_ - 1) / (this.labels_.length - 1);
  1050.         for (var i = 1; i < this.labels_.length; ++i)
  1051.           context.fillText(this.labels_[i], x, step * i);
  1052.       }
  1053.     };
  1054.  
  1055.     return Graph;
  1056.   })();
  1057.  
  1058.   return TimelineGraphView;
  1059. })();
  1060.  
  1061.  
  1062. var STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading';
  1063.  
  1064. var RECEIVED_PROPAGATION_DELTA_LABEL =
  1065.     'googReceivedPacketGroupPropagationDeltaDebug';
  1066. var RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL =
  1067.     'googReceivedPacketGroupArrivalTimeDebug';
  1068.  
  1069. // Specifies which stats should be drawn on the 'bweCompound' graph and how.
  1070. var bweCompoundGraphConfig = {
  1071.   googAvailableSendBandwidth: {color: 'red'},
  1072.   googTargetEncBitrateCorrected: {color: 'purple'},
  1073.   googActualEncBitrate: {color: 'orange'},
  1074.   googRetransmitBitrate: {color: 'blue'},
  1075.   googTransmitBitrate: {color: 'green'},
  1076. };
  1077.  
  1078. // Converts the last entry of |srcDataSeries| from the total amount to the
  1079. // amount per second.
  1080. var totalToPerSecond = function(srcDataSeries) {
  1081.   var length = srcDataSeries.dataPoints_.length;
  1082.   if (length >= 2) {
  1083.     var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
  1084.     var secondLastDataPoint = srcDataSeries.dataPoints_[length - 2];
  1085.     return (lastDataPoint.value - secondLastDataPoint.value) * 1000 /
  1086.            (lastDataPoint.time - secondLastDataPoint.time);
  1087.   }
  1088.  
  1089.   return 0;
  1090. };
  1091.  
  1092. // Converts the value of total bytes to bits per second.
  1093. var totalBytesToBitsPerSecond = function(srcDataSeries) {
  1094.   return totalToPerSecond(srcDataSeries) * 8;
  1095. };
  1096.  
  1097. // Specifies which stats should be converted before drawn and how.
  1098. // |convertedName| is the name of the converted value, |convertFunction|
  1099. // is the function used to calculate the new converted value based on the
  1100. // original dataSeries.
  1101. var dataConversionConfig = {
  1102.   packetsSent: {
  1103.     convertedName: 'packetsSentPerSecond',
  1104.     convertFunction: totalToPerSecond,
  1105.   },
  1106.   bytesSent: {
  1107.     convertedName: 'bitsSentPerSecond',
  1108.     convertFunction: totalBytesToBitsPerSecond,
  1109.   },
  1110.   packetsReceived: {
  1111.     convertedName: 'packetsReceivedPerSecond',
  1112.     convertFunction: totalToPerSecond,
  1113.   },
  1114.   bytesReceived: {
  1115.     convertedName: 'bitsReceivedPerSecond',
  1116.     convertFunction: totalBytesToBitsPerSecond,
  1117.   },
  1118.   // This is due to a bug of wrong units reported for googTargetEncBitrate.
  1119.   // TODO (jiayl): remove this when the unit bug is fixed.
  1120.   googTargetEncBitrate: {
  1121.     convertedName: 'googTargetEncBitrateCorrected',
  1122.     convertFunction: function (srcDataSeries) {
  1123.       var length = srcDataSeries.dataPoints_.length;
  1124.       var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
  1125.       if (lastDataPoint.value < 5000)
  1126.         return lastDataPoint.value * 1000;
  1127.       return lastDataPoint.value;
  1128.     }
  1129.   }
  1130. };
  1131.  
  1132.  
  1133. // The object contains the stats names that should not be added to the graph,
  1134. // even if they are numbers.
  1135. var statsNameBlackList = {
  1136.   'ssrc': true,
  1137.   'googTrackId': true,
  1138.   'googComponent': true,
  1139.   'googLocalAddress': true,
  1140.   'googRemoteAddress': true,
  1141.   'googFingerprint': true,
  1142. };
  1143.  
  1144. var graphViews = {};
  1145.  
  1146. // Returns number parsed from |value|, or NaN if the stats name is black-listed.
  1147. function getNumberFromValue(name, value) {
  1148.   if (statsNameBlackList[name])
  1149.     return NaN;
  1150.   return parseFloat(value);
  1151. }
  1152.  
  1153. // Adds the stats report |report| to the timeline graph for the given
  1154. // |peerConnectionElement|.
  1155. function drawSingleReport(peerConnectionElement, report) {
  1156.   var reportType = report.type;
  1157.   var reportId = report.id;
  1158.   var stats = report.stats;
  1159.   if (!stats || !stats.values)
  1160.     return;
  1161.  
  1162.   for (var i = 0; i < stats.values.length - 1; i = i + 2) {
  1163.     var rawLabel = stats.values[i];
  1164.     // Propagation deltas are handled separately.
  1165.     if (rawLabel == RECEIVED_PROPAGATION_DELTA_LABEL) {
  1166.       drawReceivedPropagationDelta(
  1167.           peerConnectionElement, report, stats.values[i + 1]);
  1168.       continue;
  1169.     }
  1170.     var rawDataSeriesId = reportId + '-' + rawLabel;
  1171.     var rawValue = getNumberFromValue(rawLabel, stats.values[i + 1]);
  1172.     if (isNaN(rawValue)) {
  1173.       // We do not draw non-numerical values, but still want to record it in the
  1174.       // data series.
  1175.       addDataSeriesPoints(peerConnectionElement,
  1176.                           rawDataSeriesId,
  1177.                           rawLabel,
  1178.                           [stats.timestamp],
  1179.                           [stats.values[i + 1]]);
  1180.       continue;
  1181.     }
  1182.  
  1183.     var finalDataSeriesId = rawDataSeriesId;
  1184.     var finalLabel = rawLabel;
  1185.     var finalValue = rawValue;
  1186.     // We need to convert the value if dataConversionConfig[rawLabel] exists.
  1187.     if (dataConversionConfig[rawLabel]) {
  1188.       // Updates the original dataSeries before the conversion.
  1189.       addDataSeriesPoints(peerConnectionElement,
  1190.                           rawDataSeriesId,
  1191.                           rawLabel,
  1192.                           [stats.timestamp],
  1193.                           [rawValue]);
  1194.  
  1195.       // Convert to another value to draw on graph, using the original
  1196.       // dataSeries as input.
  1197.       finalValue = dataConversionConfig[rawLabel].convertFunction(
  1198.           peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
  1199.               rawDataSeriesId));
  1200.       finalLabel = dataConversionConfig[rawLabel].convertedName;
  1201.       finalDataSeriesId = reportId + '-' + finalLabel;
  1202.     }
  1203.  
  1204.     // Updates the final dataSeries to draw.
  1205.     addDataSeriesPoints(peerConnectionElement,
  1206.                         finalDataSeriesId,
  1207.                         finalLabel,
  1208.                         [stats.timestamp],
  1209.                         [finalValue]);
  1210.  
  1211.     // Updates the graph.
  1212.     var graphType = bweCompoundGraphConfig[finalLabel] ?
  1213.                     'bweCompound' : finalLabel;
  1214.     var graphViewId =
  1215.         peerConnectionElement.id + '-' + reportId + '-' + graphType;
  1216.  
  1217.     if (!graphViews[graphViewId]) {
  1218.       graphViews[graphViewId] = createStatsGraphView(peerConnectionElement,
  1219.                                                      report,
  1220.                                                      graphType);
  1221.       var date = new Date(stats.timestamp);
  1222.       graphViews[graphViewId].setDateRange(date, date);
  1223.     }
  1224.     // Adds the new dataSeries to the graphView. We have to do it here to cover
  1225.     // both the simple and compound graph cases.
  1226.     var dataSeries =
  1227.         peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
  1228.             finalDataSeriesId);
  1229.     if (!graphViews[graphViewId].hasDataSeries(dataSeries))
  1230.       graphViews[graphViewId].addDataSeries(dataSeries);
  1231.     graphViews[graphViewId].updateEndDate();
  1232.   }
  1233. }
  1234.  
  1235. // Makes sure the TimelineDataSeries with id |dataSeriesId| is created,
  1236. // and adds the new data points to it. |times| is the list of timestamps for
  1237. // each data point, and |values| is the list of the data point values.
  1238. function addDataSeriesPoints(
  1239.     peerConnectionElement, dataSeriesId, label, times, values) {
  1240.   var dataSeries =
  1241.     peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
  1242.         dataSeriesId);
  1243.   if (!dataSeries) {
  1244.     dataSeries = new TimelineDataSeries();
  1245.     peerConnectionDataStore[peerConnectionElement.id].setDataSeries(
  1246.         dataSeriesId, dataSeries);
  1247.     if (bweCompoundGraphConfig[label]) {
  1248.       dataSeries.setColor(bweCompoundGraphConfig[label].color);
  1249.     }
  1250.   }
  1251.   for (var i = 0; i < times.length; ++i)
  1252.     dataSeries.addPoint(times[i], values[i]);
  1253. }
  1254.  
  1255. // Draws the received propagation deltas using the packet group arrival time as
  1256. // the x-axis. For example, |report.stats.values| should be like
  1257. // ['googReceivedPacketGroupArrivalTimeDebug', '[123456, 234455, 344566]',
  1258. //  'googReceivedPacketGroupPropagationDeltaDebug', '[23, 45, 56]', ...].
  1259. function drawReceivedPropagationDelta(peerConnectionElement, report, deltas) {
  1260.   var reportId = report.id;
  1261.   var stats = report.stats;
  1262.   var times = null;
  1263.   // Find the packet group arrival times.
  1264.   for (var i = 0; i < stats.values.length - 1; i = i + 2) {
  1265.     if (stats.values[i] == RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL) {
  1266.       times = stats.values[i + 1];
  1267.       break;
  1268.     }
  1269.   }
  1270.   // Unexpected.
  1271.   if (times == null)
  1272.     return;
  1273.  
  1274.   // Convert |deltas| and |times| from strings to arrays of numbers.
  1275.   try {
  1276.     deltas = JSON.parse(deltas);
  1277.     times = JSON.parse(times);
  1278.   } catch (e) {
  1279.     console.log(e);
  1280.     return;
  1281.   }
  1282.  
  1283.   // Update the data series.
  1284.   var dataSeriesId = reportId + '-' + RECEIVED_PROPAGATION_DELTA_LABEL;
  1285.   addDataSeriesPoints(
  1286.       peerConnectionElement,
  1287.       dataSeriesId,
  1288.       RECEIVED_PROPAGATION_DELTA_LABEL,
  1289.       times,
  1290.       deltas);
  1291.   // Update the graph.
  1292.   var graphViewId = peerConnectionElement.id + '-' + reportId + '-' +
  1293.       RECEIVED_PROPAGATION_DELTA_LABEL;
  1294.   var date = new Date(times[times.length - 1]);
  1295.   if (!graphViews[graphViewId]) {
  1296.     graphViews[graphViewId] = createStatsGraphView(
  1297.         peerConnectionElement,
  1298.         report,
  1299.         RECEIVED_PROPAGATION_DELTA_LABEL);
  1300.     graphViews[graphViewId].setScale(10);
  1301.     graphViews[graphViewId].setDateRange(date, date);
  1302.     var dataSeries = peerConnectionDataStore[peerConnectionElement.id]
  1303.         .getDataSeries(dataSeriesId);
  1304.     graphViews[graphViewId].addDataSeries(dataSeries);
  1305.   }
  1306.   graphViews[graphViewId].updateEndDate(date);
  1307. }
  1308.  
  1309. // Ensures a div container to hold all stats graphs for one track is created as
  1310. // a child of |peerConnectionElement|.
  1311. function ensureStatsGraphTopContainer(peerConnectionElement, report) {
  1312.   var containerId = peerConnectionElement.id + '-' +
  1313.       report.type + '-' + report.id + '-graph-container';
  1314.   var container = $(containerId);
  1315.   if (!container) {
  1316.     container = document.createElement('details');
  1317.     container.id = containerId;
  1318.     container.className = 'stats-graph-container';
  1319.  
  1320.     peerConnectionElement.appendChild(container);
  1321.     container.innerHTML ='<summary><span></span></summary>';
  1322.     container.firstChild.firstChild.className =
  1323.         STATS_GRAPH_CONTAINER_HEADING_CLASS;
  1324.     container.firstChild.firstChild.textContent =
  1325.         'Stats graphs for ' + report.id;
  1326.  
  1327.     if (report.type == 'ssrc') {
  1328.       var ssrcInfoElement = document.createElement('div');
  1329.       container.firstChild.appendChild(ssrcInfoElement);
  1330.       ssrcInfoManager.populateSsrcInfo(ssrcInfoElement,
  1331.                                        GetSsrcFromReport(report));
  1332.     }
  1333.   }
  1334.   return container;
  1335. }
  1336.  
  1337. // Creates the container elements holding a timeline graph
  1338. // and the TimelineGraphView object.
  1339. function createStatsGraphView(
  1340.     peerConnectionElement, report, statsName) {
  1341.   var topContainer = ensureStatsGraphTopContainer(peerConnectionElement,
  1342.                                                   report);
  1343.  
  1344.   var graphViewId =
  1345.       peerConnectionElement.id + '-' + report.id + '-' + statsName;
  1346.   var divId = graphViewId + '-div';
  1347.   var canvasId = graphViewId + '-canvas';
  1348.   var container = document.createElement("div");
  1349.   container.className = 'stats-graph-sub-container';
  1350.  
  1351.   topContainer.appendChild(container);
  1352.   container.innerHTML = '<div>' + statsName + '</div>' +
  1353.       '<div id=' + divId + '><canvas id=' + canvasId + '></canvas></div>';
  1354.   if (statsName == 'bweCompound') {
  1355.       container.insertBefore(
  1356.           createBweCompoundLegend(peerConnectionElement, report.id),
  1357.           $(divId));
  1358.   }
  1359.   return new TimelineGraphView(divId, canvasId);
  1360. }
  1361.  
  1362. // Creates the legend section for the bweCompound graph.
  1363. // Returns the legend element.
  1364. function createBweCompoundLegend(peerConnectionElement, reportId) {
  1365.   var legend = document.createElement('div');
  1366.   for (var prop in bweCompoundGraphConfig) {
  1367.     var div = document.createElement('div');
  1368.     legend.appendChild(div);
  1369.     div.innerHTML = '<input type=checkbox checked></input>' + prop;
  1370.     div.style.color = bweCompoundGraphConfig[prop].color;
  1371.     div.dataSeriesId = reportId + '-' + prop;
  1372.     div.graphViewId =
  1373.         peerConnectionElement.id + '-' + reportId + '-bweCompound';
  1374.     div.firstChild.addEventListener('click', function(event) {
  1375.         var target =
  1376.             peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
  1377.                 event.target.parentNode.dataSeriesId);
  1378.         target.show(event.target.checked);
  1379.         graphViews[event.target.parentNode.graphViewId].repaint();
  1380.     });
  1381.   }
  1382.   return legend;
  1383. }
  1384.  
  1385. // Copyright (c) 2013 The Chromium Authors. All rights reserved.
  1386. // Use of this source code is governed by a BSD-style license that can be
  1387. // found in the LICENSE file.
  1388.  
  1389.  
  1390. /**
  1391.  * Maintains the stats table.
  1392.  * @param {SsrcInfoManager} ssrcInfoManager The source of the ssrc info.
  1393.  */
  1394. var StatsTable = (function(ssrcInfoManager) {
  1395.   'use strict';
  1396.  
  1397.   /**
  1398.    * @param {SsrcInfoManager} ssrcInfoManager The source of the ssrc info.
  1399.    * @constructor
  1400.    */
  1401.   function StatsTable(ssrcInfoManager) {
  1402.     /**
  1403.      * @type {SsrcInfoManager}
  1404.      * @private
  1405.      */
  1406.     this.ssrcInfoManager_ = ssrcInfoManager;
  1407.   }
  1408.  
  1409.   StatsTable.prototype = {
  1410.     /**
  1411.      * Adds |report| to the stats table of |peerConnectionElement|.
  1412.      *
  1413.      * @param {!Element} peerConnectionElement The root element.
  1414.      * @param {!Object} report The object containing stats, which is the object
  1415.      *     containing timestamp and values, which is an array of strings, whose
  1416.      *     even index entry is the name of the stat, and the odd index entry is
  1417.      *     the value.
  1418.      */
  1419.     addStatsReport: function(peerConnectionElement, report) {
  1420.       var statsTable = this.ensureStatsTable_(peerConnectionElement, report);
  1421.  
  1422.       if (report.stats) {
  1423.         this.addStatsToTable_(statsTable,
  1424.                               report.stats.timestamp, report.stats.values);
  1425.       }
  1426.     },
  1427.  
  1428.     /**
  1429.      * Ensure the DIV container for the stats tables is created as a child of
  1430.      * |peerConnectionElement|.
  1431.      *
  1432.      * @param {!Element} peerConnectionElement The root element.
  1433.      * @return {!Element} The stats table container.
  1434.      * @private
  1435.      */
  1436.     ensureStatsTableContainer_: function(peerConnectionElement) {
  1437.       var containerId = peerConnectionElement.id + '-table-container';
  1438.       var container = $(containerId);
  1439.       if (!container) {
  1440.         container = document.createElement('div');
  1441.         container.id = containerId;
  1442.         container.className = 'stats-table-container';
  1443.         var head = document.createElement('div');
  1444.         head.textContent = 'Stats Tables';
  1445.         container.appendChild(head);
  1446.         peerConnectionElement.appendChild(container);
  1447.       }
  1448.       return container;
  1449.     },
  1450.  
  1451.     /**
  1452.      * Ensure the stats table for track specified by |report| of PeerConnection
  1453.      * |peerConnectionElement| is created.
  1454.      *
  1455.      * @param {!Element} peerConnectionElement The root element.
  1456.      * @param {!Object} report The object containing stats, which is the object
  1457.      *     containing timestamp and values, which is an array of strings, whose
  1458.      *     even index entry is the name of the stat, and the odd index entry is
  1459.      *     the value.
  1460.      * @return {!Element} The stats table element.
  1461.      * @private
  1462.      */
  1463.      ensureStatsTable_: function(peerConnectionElement, report) {
  1464.       var tableId = peerConnectionElement.id + '-table-' + report.id;
  1465.       var table = $(tableId);
  1466.       if (!table) {
  1467.         var container = this.ensureStatsTableContainer_(peerConnectionElement);
  1468.         var details = document.createElement('details');
  1469.         container.appendChild(details);
  1470.  
  1471.         var summary = document.createElement('summary');
  1472.         summary.textContent = report.id;
  1473.         details.appendChild(summary);
  1474.  
  1475.         table = document.createElement('table');
  1476.         details.appendChild(table);
  1477.         table.id = tableId;
  1478.         table.border = 1;
  1479.  
  1480.         table.innerHTML = '<tr><th colspan=2></th></tr>';
  1481.         table.rows[0].cells[0].textContent = 'Statistics ' + report.id;
  1482.         if (report.type == 'ssrc') {
  1483.             table.insertRow(1);
  1484.             table.rows[1].innerHTML = '<td colspan=2></td>';
  1485.             this.ssrcInfoManager_.populateSsrcInfo(
  1486.                 table.rows[1].cells[0], GetSsrcFromReport(report));
  1487.         }
  1488.       }
  1489.       return table;
  1490.     },
  1491.  
  1492.     /**
  1493.      * Update |statsTable| with |time| and |statsData|.
  1494.      *
  1495.      * @param {!Element} statsTable Which table to update.
  1496.      * @param {number} time The number of miliseconds since epoch.
  1497.      * @param {Array.<string>} statsData An array of stats name and value pairs.
  1498.      * @private
  1499.      */
  1500.     addStatsToTable_: function(statsTable, time, statsData) {
  1501.       var date = new Date(time);
  1502.       this.updateStatsTableRow_(statsTable, 'timestamp', date.toLocaleString());
  1503.       for (var i = 0; i < statsData.length - 1; i = i + 2) {
  1504.         this.updateStatsTableRow_(statsTable, statsData[i], statsData[i + 1]);
  1505.       }
  1506.     },
  1507.  
  1508.     /**
  1509.      * Update the value column of the stats row of |rowName| to |value|.
  1510.      * A new row is created is this is the first report of this stats.
  1511.      *
  1512.      * @param {!Element} statsTable Which table to update.
  1513.      * @param {string} rowName The name of the row to update.
  1514.      * @param {string} value The new value to set.
  1515.      * @private
  1516.      */
  1517.     updateStatsTableRow_: function(statsTable, rowName, value) {
  1518.       var trId = statsTable.id + '-' + rowName;
  1519.       var trElement = $(trId);
  1520.       if (!trElement) {
  1521.         trElement = document.createElement('tr');
  1522.         trElement.id = trId;
  1523.         statsTable.firstChild.appendChild(trElement);
  1524.         trElement.innerHTML = '<td>' + rowName + '</td><td></td>';
  1525.       }
  1526.       trElement.cells[1].textContent = value;
  1527.  
  1528.       // Highlights the table for the active connection.
  1529.       if (rowName == 'googActiveConnection' && value == 'true')
  1530.         statsTable.parentElement.classList.add('stats-table-active-connection');
  1531.     }
  1532.   };
  1533.  
  1534.   return StatsTable;
  1535. })();
  1536.  
  1537. // Copyright (c) 2013 The Chromium Authors. All rights reserved.
  1538. // Use of this source code is governed by a BSD-style license that can be
  1539. // found in the LICENSE file.
  1540.  
  1541.  
  1542. /**
  1543.  * The data of a peer connection update.
  1544.  * @param {number} pid The id of the renderer.
  1545.  * @param {number} lid The id of the peer conneciton inside a renderer.
  1546.  * @param {string} type The type of the update.
  1547.  * @param {string} value The details of the update.
  1548.  * @constructor
  1549.  */
  1550. var PeerConnectionUpdateEntry = function(pid, lid, type, value) {
  1551.   /**
  1552.    * @type {number}
  1553.    */
  1554.   this.pid = pid;
  1555.  
  1556.   /**
  1557.    * @type {number}
  1558.    */
  1559.   this.lid = lid;
  1560.  
  1561.   /**
  1562.    * @type {string}
  1563.    */
  1564.   this.type = type;
  1565.  
  1566.   /**
  1567.    * @type {string}
  1568.    */
  1569.   this.value = value;
  1570. };
  1571.  
  1572.  
  1573. /**
  1574.  * Maintains the peer connection update log table.
  1575.  */
  1576. var PeerConnectionUpdateTable = (function() {
  1577.   'use strict';
  1578.  
  1579.   /**
  1580.    * @constructor
  1581.    */
  1582.   function PeerConnectionUpdateTable() {
  1583.     /**
  1584.      * @type {string}
  1585.      * @const
  1586.      * @private
  1587.      */
  1588.     this.UPDATE_LOG_ID_SUFFIX_ = '-update-log';
  1589.  
  1590.     /**
  1591.      * @type {string}
  1592.      * @const
  1593.      * @private
  1594.      */
  1595.     this.UPDATE_LOG_CONTAINER_CLASS_ = 'update-log-container';
  1596.  
  1597.     /**
  1598.      * @type {string}
  1599.      * @const
  1600.      * @private
  1601.      */
  1602.     this.UPDATE_LOG_TABLE_CLASS = 'update-log-table';
  1603.   }
  1604.  
  1605.   PeerConnectionUpdateTable.prototype = {
  1606.     /**
  1607.      * Adds the update to the update table as a new row. The type of the update
  1608.      * is set to the summary of the cell; clicking the cell will reveal or hide
  1609.      * the details as the content of a TextArea element.
  1610.      *
  1611.      * @param {!Element} peerConnectionElement The root element.
  1612.      * @param {!PeerConnectionUpdateEntry} update The update to add.
  1613.      */
  1614.     addPeerConnectionUpdate: function(peerConnectionElement, update) {
  1615.       var tableElement = this.ensureUpdateContainer_(peerConnectionElement);
  1616.  
  1617.       var row = document.createElement('tr');
  1618.       tableElement.firstChild.appendChild(row);
  1619.  
  1620.       var time = new Date(parseFloat(update.time));
  1621.       row.innerHTML = '<td>' + time.toLocaleString() + '</td>';
  1622.  
  1623.       if (update.value.length == 0) {
  1624.         row.innerHTML += '<td>' + update.type + '</td>';
  1625.         return;
  1626.       }
  1627.  
  1628.       row.innerHTML += '<td><details><summary>' + update.type +
  1629.           '</summary></details></td>';
  1630.  
  1631.       var valueContainer = document.createElement('pre');
  1632.       var details = row.cells[1].childNodes[0];
  1633.       details.appendChild(valueContainer);
  1634.       valueContainer.textContent = update.value;
  1635.     },
  1636.  
  1637.     /**
  1638.      * Makes sure the update log table of the peer connection is created.
  1639.      *
  1640.      * @param {!Element} peerConnectionElement The root element.
  1641.      * @return {!Element} The log table element.
  1642.      * @private
  1643.      */
  1644.     ensureUpdateContainer_: function(peerConnectionElement) {
  1645.       var tableId = peerConnectionElement.id + this.UPDATE_LOG_ID_SUFFIX_;
  1646.       var tableElement = $(tableId);
  1647.       if (!tableElement) {
  1648.         var tableContainer = document.createElement('div');
  1649.         tableContainer.className = this.UPDATE_LOG_CONTAINER_CLASS_;
  1650.         peerConnectionElement.appendChild(tableContainer);
  1651.  
  1652.         tableElement = document.createElement('table');
  1653.         tableElement.className = this.UPDATE_LOG_TABLE_CLASS;
  1654.         tableElement.id = tableId;
  1655.         tableElement.border = 1;
  1656.         tableContainer.appendChild(tableElement);
  1657.         tableElement.innerHTML = '<tr><th>Time</th>' +
  1658.             '<th class="update-log-header-event">Event</th></tr>';
  1659.       }
  1660.       return tableElement;
  1661.     }
  1662.   };
  1663.  
  1664.   return PeerConnectionUpdateTable;
  1665. })();
  1666.  
  1667. // Copyright (c) 2013 The Chromium Authors. All rights reserved.
  1668. // Use of this source code is governed by a BSD-style license that can be
  1669. // found in the LICENSE file.
  1670.  
  1671.  
  1672. /**
  1673.  * Provides the UI for dump creation.
  1674.  */
  1675. var DumpCreator = (function() {
  1676.   /**
  1677.    * @param {Element} containerElement The parent element of the dump creation
  1678.    *     UI.
  1679.    * @constructor
  1680.    */
  1681.   function DumpCreator(containerElement) {
  1682.     /**
  1683.      * The root element of the dump creation UI.
  1684.      * @type {Element}
  1685.      * @private
  1686.      */
  1687.     this.root_ = document.createElement('details');
  1688.  
  1689.     this.root_.className = 'peer-connection-dump-root';
  1690.     containerElement.appendChild(this.root_);
  1691.     var summary = document.createElement('summary');
  1692.     this.root_.appendChild(summary);
  1693.     summary.textContent = 'Create Dump';
  1694.     var content = document.createElement('div');
  1695.     this.root_.appendChild(content);
  1696.  
  1697.     content.innerHTML = '<div><a><button>' +
  1698.         'Download the PeerConnection updates and stats data' +
  1699.         '</button></a></div>' +
  1700.         '<p><label><input type=checkbox>' +
  1701.         'Enable diagnostic audio recordings.</label></p>' +
  1702.         '<p>A diagnostic audio recording is used for analyzing audio' +
  1703.         ' problems. It contains the audio played out from the speaker and' +
  1704.         ' recorded from the microphone and is saved to the local disk.' +
  1705.         ' Checking this box will enable the recording for ongoing WebRTC' +
  1706.         ' calls and for future WebRTC calls. When the box is unchecked or' +
  1707.         ' this page is closed, all ongoing recordings will be stopped and' +
  1708.         ' this recording functionality will be disabled for future WebRTC' +
  1709.         ' calls. Recordings in multiple tabs are supported as well as' +
  1710.         ' multiple recordings in the same tab. When enabling, you select a' +
  1711.         ' base filename to save the dump(s) to. The base filename will have a' +
  1712.         ' suffix appended to it as <base filename>.<render process' +
  1713.         ' ID>.<recording ID>. If recordings are' +
  1714.         ' disabled and then enabled using the same base filename, the' +
  1715.         ' file(s) will be appended to and may become invalid. It is' +
  1716.         ' recommended to choose a new base filename each time or move' +
  1717.         ' the resulting files before enabling again. If track processing is' +
  1718.         ' disabled (--disable-audio-track-processing): (1) Only one recording' +
  1719.         ' per render process is supported. (2) When the box is unchecked or' +
  1720.         ' this page is closed, ongoing recordings will continue until the' +
  1721.         ' call ends or the page with the recording is closed.</p>';
  1722.  
  1723.     content.getElementsByTagName('a')[0].addEventListener(
  1724.         'click', this.onDownloadData_.bind(this));
  1725.     content.getElementsByTagName('input')[0].addEventListener(
  1726.         'click', this.onAecRecordingChanged_.bind(this));
  1727.   }
  1728.  
  1729.   DumpCreator.prototype = {
  1730.     // Mark the AEC recording checkbox checked.
  1731.     enableAecRecording: function() {
  1732.       this.root_.getElementsByTagName('input')[0].checked = true;
  1733.     },
  1734.  
  1735.     // Mark the AEC recording checkbox unchecked.
  1736.     disableAecRecording: function() {
  1737.       this.root_.getElementsByTagName('input')[0].checked = false;
  1738.     },
  1739.  
  1740.     /**
  1741.      * Downloads the PeerConnection updates and stats data as a file.
  1742.      *
  1743.      * @private
  1744.      */
  1745.     onDownloadData_: function() {
  1746.       var dump_object =
  1747.       {
  1748.         'getUserMedia': userMediaRequests,
  1749.         'PeerConnections': peerConnectionDataStore,
  1750.       };
  1751.       var textBlob = new Blob([JSON.stringify(dump_object, null, ' ')],
  1752.                               {type: 'octet/stream'});
  1753.       var URL = window.URL.createObjectURL(textBlob);
  1754.  
  1755.       var anchor = this.root_.getElementsByTagName('a')[0];
  1756.       anchor.href = URL;
  1757.       anchor.download = 'webrtc_internals_dump.txt';
  1758.       // The default action of the anchor will download the URL.
  1759.     },
  1760.  
  1761.     /**
  1762.      * Handles the event of toggling the AEC recording state.
  1763.      *
  1764.      * @private
  1765.      */
  1766.     onAecRecordingChanged_: function() {
  1767.       var enabled = this.root_.getElementsByTagName('input')[0].checked;
  1768.       if (enabled) {
  1769.         chrome.send('enableAecRecording');
  1770.       } else {
  1771.         chrome.send('disableAecRecording');
  1772.       }
  1773.     },
  1774.   };
  1775.   return DumpCreator;
  1776. })();
  1777.  
  1778.  
  1779.  
  1780. function initialize() {
  1781.   dumpCreator = new DumpCreator($('content-root'));
  1782.   tabView = new TabView($('content-root'));
  1783.   ssrcInfoManager = new SsrcInfoManager();
  1784.   peerConnectionUpdateTable = new PeerConnectionUpdateTable();
  1785.   statsTable = new StatsTable(ssrcInfoManager);
  1786.  
  1787.   chrome.send('finishedDOMLoad');
  1788.  
  1789.   // Requests stats from all peer connections every second.
  1790.   window.setInterval(requestStats, 1000);
  1791. }
  1792. document.addEventListener('DOMContentLoaded', initialize);
  1793.  
  1794.  
  1795. /** Sends a request to the browser to get peer connection statistics. */
  1796. function requestStats() {
  1797.   if (Object.keys(peerConnectionDataStore).length > 0)
  1798.     chrome.send('getAllStats');
  1799. }
  1800.  
  1801.  
  1802. /**
  1803.  * A helper function for getting a peer connection element id.
  1804.  *
  1805.  * @param {!Object.<string, number>} data The object containing the pid and lid
  1806.  *     of the peer connection.
  1807.  * @return {string} The peer connection element id.
  1808.  */
  1809. function getPeerConnectionId(data) {
  1810.   return data.pid + '-' + data.lid;
  1811. }
  1812.  
  1813.  
  1814. /**
  1815.  * Extracts ssrc info from a setLocal/setRemoteDescription update.
  1816.  *
  1817.  * @param {!PeerConnectionUpdateEntry} data The peer connection update data.
  1818.  */
  1819. function extractSsrcInfo(data) {
  1820.   if (data.type == 'setLocalDescription' ||
  1821.       data.type == 'setRemoteDescription') {
  1822.     ssrcInfoManager.addSsrcStreamInfo(data.value);
  1823.   }
  1824. }
  1825.  
  1826.  
  1827. /**
  1828.  * A helper function for appending a child element to |parent|.
  1829.  *
  1830.  * @param {!Element} parent The parent element.
  1831.  * @param {string} tag The child element tag.
  1832.  * @param {string} text The textContent of the new DIV.
  1833.  * @return {!Element} the new DIV element.
  1834.  */
  1835. function appendChildWithText(parent, tag, text) {
  1836.   var child = document.createElement(tag);
  1837.   child.textContent = text;
  1838.   parent.appendChild(child);
  1839.   return child;
  1840. }
  1841.  
  1842. /**
  1843.  * Helper for adding a peer connection update.
  1844.  *
  1845.  * @param {Element} peerConnectionElement
  1846.  * @param {!PeerConnectionUpdateEntry} update The peer connection update data.
  1847.  */
  1848. function addPeerConnectionUpdate(peerConnectionElement, update) {
  1849.   peerConnectionUpdateTable.addPeerConnectionUpdate(peerConnectionElement,
  1850.                                                     update);
  1851.   extractSsrcInfo(update);
  1852.   peerConnectionDataStore[peerConnectionElement.id].addUpdate(update);
  1853. }
  1854.  
  1855.  
  1856. /** Browser message handlers. */
  1857.  
  1858.  
  1859. /**
  1860.  * Removes all information about a peer connection.
  1861.  *
  1862.  * @param {!Object.<string, number>} data The object containing the pid and lid
  1863.  *     of a peer connection.
  1864.  */
  1865. function removePeerConnection(data) {
  1866.   var element = $(getPeerConnectionId(data));
  1867.   if (element) {
  1868.     delete peerConnectionDataStore[element.id];
  1869.     tabView.removeTab(element.id);
  1870.   }
  1871. }
  1872.  
  1873.  
  1874. /**
  1875.  * Adds a peer connection.
  1876.  *
  1877.  * @param {!Object} data The object containing the pid, lid, url,
  1878.  *     rtcConfiguration, and constraints of a peer connection.
  1879.  */
  1880. function addPeerConnection(data) {
  1881.   var id = getPeerConnectionId(data);
  1882.  
  1883.   if (!peerConnectionDataStore[id]) {
  1884.     peerConnectionDataStore[id] = new PeerConnectionRecord();
  1885.   }
  1886.   peerConnectionDataStore[id].initialize(
  1887.       data.url, data.rtcConfiguration, data.constraints);
  1888.  
  1889.   var peerConnectionElement = $(id);
  1890.   if (!peerConnectionElement) {
  1891.     peerConnectionElement = tabView.addTab(id, data.url + ' [' + id + ']');
  1892.   }
  1893.   peerConnectionElement.innerHTML =
  1894.       '<p>' + data.url + ' ' + data.rtcConfiguration + ' ' + data.constraints +
  1895.       '</p>';
  1896.  
  1897.   return peerConnectionElement;
  1898. }
  1899.  
  1900.  
  1901. /**
  1902.  * Adds a peer connection update.
  1903.  *
  1904.  * @param {!PeerConnectionUpdateEntry} data The peer connection update data.
  1905.  */
  1906. function updatePeerConnection(data) {
  1907.   var peerConnectionElement = $(getPeerConnectionId(data));
  1908.   addPeerConnectionUpdate(peerConnectionElement, data);
  1909. }
  1910.  
  1911.  
  1912. /**
  1913.  * Adds the information of all peer connections created so far.
  1914.  *
  1915.  * @param {Array.<!Object>} data An array of the information of all peer
  1916.  *     connections. Each array item contains pid, lid, url, rtcConfiguration,
  1917.  *     constraints, and an array of updates as the log.
  1918.  */
  1919. function updateAllPeerConnections(data) {
  1920.   for (var i = 0; i < data.length; ++i) {
  1921.     var peerConnection = addPeerConnection(data[i]);
  1922.  
  1923.     var log = data[i].log;
  1924.     if (!log)
  1925.       continue;
  1926.     for (var j = 0; j < log.length; ++j) {
  1927.       addPeerConnectionUpdate(peerConnection, log[j]);
  1928.     }
  1929.   }
  1930.   requestStats();
  1931. }
  1932.  
  1933.  
  1934. /**
  1935.  * Handles the report of stats.
  1936.  *
  1937.  * @param {!Object} data The object containing pid, lid, and reports, where
  1938.  *     reports is an array of stats reports. Each report contains id, type,
  1939.  *     and stats, where stats is the object containing timestamp and values,
  1940.  *     which is an array of strings, whose even index entry is the name of the
  1941.  *     stat, and the odd index entry is the value.
  1942.  */
  1943. function addStats(data) {
  1944.   var peerConnectionElement = $(getPeerConnectionId(data));
  1945.   if (!peerConnectionElement)
  1946.     return;
  1947.  
  1948.   for (var i = 0; i < data.reports.length; ++i) {
  1949.     var report = data.reports[i];
  1950.     statsTable.addStatsReport(peerConnectionElement, report);
  1951.     drawSingleReport(peerConnectionElement, report);
  1952.   }
  1953. }
  1954.  
  1955.  
  1956. /**
  1957.  * Adds a getUserMedia request.
  1958.  *
  1959.  * @param {!Object} data The object containing rid {number}, pid {number},
  1960.  *     origin {string}, audio {string}, video {string}.
  1961.  */
  1962. function addGetUserMedia(data) {
  1963.   userMediaRequests.push(data);
  1964.  
  1965.   if (!$(USER_MEDIA_TAB_ID)) {
  1966.     tabView.addTab(USER_MEDIA_TAB_ID, 'GetUserMedia Requests');
  1967.   }
  1968.  
  1969.   var requestDiv = document.createElement('div');
  1970.   requestDiv.className = 'user-media-request-div-class';
  1971.   requestDiv.rid = data.rid;
  1972.   $(USER_MEDIA_TAB_ID).appendChild(requestDiv);
  1973.  
  1974.   appendChildWithText(requestDiv, 'div', 'Caller origin: ' + data.origin);
  1975.   appendChildWithText(requestDiv, 'div', 'Caller process id: ' + data.pid);
  1976.   appendChildWithText(requestDiv, 'span', 'Audio Constraints').style.fontWeight
  1977.       = 'bold';
  1978.   appendChildWithText(requestDiv, 'div', data.audio);
  1979.  
  1980.   appendChildWithText(requestDiv, 'span', 'Video Constraints').style.fontWeight
  1981.       = 'bold';
  1982.   appendChildWithText(requestDiv, 'div', data.video);
  1983. }
  1984.  
  1985.  
  1986. /**
  1987.  * Removes the getUserMedia requests from the specified |rid|.
  1988.  *
  1989.  * @param {!Object} data The object containing rid {number}, the render id.
  1990.  */
  1991. function removeGetUserMediaForRenderer(data) {
  1992.   for (var i = userMediaRequests.length - 1; i >= 0; --i) {
  1993.     if (userMediaRequests[i].rid == data.rid)
  1994.       userMediaRequests.splice(i, 1);
  1995.   }
  1996.  
  1997.   var requests = $(USER_MEDIA_TAB_ID).childNodes;
  1998.   for (var i = 0; i < requests.length; ++i) {
  1999.     if (requests[i].rid == data.rid)
  2000.       $(USER_MEDIA_TAB_ID).removeChild(requests[i]);
  2001.  
  2002.   }
  2003.   if ($(USER_MEDIA_TAB_ID).childNodes.length == 0)
  2004.     tabView.removeTab(USER_MEDIA_TAB_ID);
  2005. }
  2006.  
  2007.  
  2008. /**
  2009.  * Notification that the AEC recording file selection dialog was cancelled,
  2010.  * i.e. AEC has not been enabled.
  2011.  */
  2012. function aecRecordingFileSelectionCancelled() {
  2013.   dumpCreator.disableAecRecording();
  2014. }
  2015.  
  2016.  
  2017. /**
  2018.  * Set
  2019.  */
  2020. function enableAecRecording() {
  2021.   dumpCreator.enableAecRecording();
  2022. }
  2023.